Skip to content

perf(workflow): scope resize CSS-var writes to the container, not :root#5738

Merged
waleedlatif1 merged 5 commits into
stagingfrom
canvas-resize-perf
Jul 17, 2026
Merged

perf(workflow): scope resize CSS-var writes to the container, not :root#5738
waleedlatif1 merged 5 commits into
stagingfrom
canvas-resize-perf

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Resizing the editor panel, terminal, output panel, and sidebar on the workflow canvas was still laggy on real workflows even after the drag handles stopped re-rendering React per frame (improvement(workflow): zero-render drag-resize for panel, terminal, and output panel #5730). A Chrome DevTools CPU/tracing capture showed the remaining cost is style recalculation, not JS or layout: ~3.9s of Document::recalcStyle over a 50-move panel drag.
  • Root cause: each drag frame wrote its CSS variable to document.documentElement. On a large document (~42k elements) any inline custom-property write on the <html> root recalculates style for the whole tree — measured at ~77ms per write, independent of what reads the variable (an unused --zzz var costs the same 77ms). Writing the same variable to the element that consumes it recalcs only that subtree (~0.5ms — a ~150x reduction).
  • Fix: useDragResize now writes the variable to a caller-provided target element during the drag and reconciles to :root once on release (so on-demand readers, the pre-hydration blocking script, and rehydration are all unchanged). panel → .panel-container, terminal → .terminal-container, output panel → .terminal-container (both the panel and logs column inherit it), sidebar → .sidebar-shell-outer.
  • The terminal's expanded-threshold sync writes store state directly instead of setTerminalHeight so it no longer touches :root mid-drag (that was a ~77ms hitch right at the collapse threshold).

Measured (near-empty canvas, 50-move drag, CDP trace + rAF frame timing)

Handle Before After
Panel style+layout 3891ms 56ms
Panel frame p95 / max 91.6 / 108ms 8.4 / 8.8ms
Panel main-thread blocking 4566ms 0ms
Terminal recalcStyle 3899ms 16ms
Terminal blocking 5558ms 0ms
Sidebar recalc ~77ms/frame ~1ms/frame

Frames drop from ~11fps (91ms) to a solid 120fps (8.3ms) with zero long-tasks.

Type of Change

  • Performance improvement

Testing

Verified with Playwright + CDP against a live dev workflow: all four handles engage, drive their element's width/height live during the drag (scoped inline var), commit the final value to :root + the store on release, remove the scoped override (reconciled), and persist. Before/after CPU profiles and traces captured for each handle.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

Resizing the editor panel, terminal, output panel, and sidebar was still
laggy on large workflows even after the drag handles stopped re-rendering
React per frame. A CDP trace showed the cost was style recalculation, not
JS or layout: ~3.9s of Document::recalcStyle over a 50-move panel drag.

Root cause: each drag frame wrote its CSS variable to document.documentElement.
On a large document (~42k elements) any inline custom-property write on the
<html> root recalculates style for the whole tree — measured at ~77ms per
write, independent of what actually reads the variable (an unused var costs
the same). Writing the same variable to the element that consumes it recalcs
only that subtree (~0.5ms) — a ~150x reduction.

useDragResize now writes the variable to a caller-provided target element
during the drag and reconciles to :root once on release (so on-demand readers
and the pre-hydration script are unchanged): panel -> .panel-container,
terminal -> .terminal-container, output panel -> .terminal-container (both
consumers inherit it), sidebar -> .sidebar-shell-outer. The terminal's
expanded-threshold sync writes store state directly instead of setTerminalHeight
so it no longer touches :root mid-drag.

Measured (near-empty canvas, 50-move drag): panel style+layout 3891ms -> 56ms,
frame p95 91.6ms -> 8.4ms, main-thread blocking 4566ms -> 0ms; terminal
recalc 3899ms -> 16ms, blocking 5558ms -> 0ms; sidebar recalc ~77ms/frame -> ~1ms/frame.
@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 17, 2026 8:02pm

Request Review

@cursor

cursor Bot commented Jul 17, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches shared drag-resize behavior and layout CSS variable sources across sidebar, panel, terminal, and floats; regressions could show as wrong sizes, stuck overrides, or floats not tracking boundaries during drag.

Overview
Workflow chrome resize drags were still expensive because each frame wrote layout CSS variables on :root, forcing full-document style recalculation on large canvases.

useDragResize now takes cssVar and getTarget and applies the variable on the consuming element during the drag, then **commit**s to the store (which still sets :root at rest) and removes the scoped inline override. Unmount mid-drag finalizes the same way as pointer release so overrides cannot stick on long-lived chrome nodes.

Panel, terminal, output panel, and sidebar hooks are wired to .panel-container, .terminal-container, and .sidebar-shell-outer respectively. Terminal expanded/collapsed threshold sync uses direct store updates in onApply so it does not call setTerminalHeight (and hit :root) mid-drag.

useFloatBoundarySync prefers inline overrides on those containers while dragging and observes their style mutations so floats re-clamp live. Terminal output-panel auto-clamp on resize bails out while an output-panel drag owns --output-panel-width on the terminal element.

Reviewed by Cursor Bugbot for commit 4a8b2b9. Configure here.

Comment thread apps/sim/hooks/use-drag-resize.ts
@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR reduces workflow-canvas resize costs by scoping live CSS-variable updates to their consuming containers. The main changes are:

  • Scoped panel, terminal, output-panel, and sidebar resize writes.
  • Reconciled final dimensions to the store and :root after each drag.
  • Preserved final values when release or unmount cancels a pending frame.
  • Updated terminal and floating-boundary logic to read live scoped values.

Confidence Score: 5/5

This looks safe to merge.

  • Active resize teardown now commits the last displayed value and removes scoped overrides.
  • Sidebar release preserves the final pointer position even when a pending frame is cancelled.
  • No blocking issues remain in the updated paths.

Important Files Changed

Filename Overview
apps/sim/hooks/use-drag-resize.ts Scopes live CSS-variable writes and reconciles the last applied value during release or unmount.
apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-sidebar-resize.ts Captures the final clamped pointer width before scheduling the DOM update.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx Skips store-driven output-width clamping while a scoped drag override is active.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/float/use-float-boundary-sync.ts Reads and observes scoped boundary dimensions during live resize operations.

Reviews (5): Last reviewed commit: "fix(workflow): sidebar flick-safety + cl..." | Re-trigger Greptile

Comment thread apps/sim/hooks/use-drag-resize.ts
The resize hooks now write their CSS variable to the consuming container
element during a drag (not :root), so use-float-boundary-sync's MutationObserver
on :root no longer fired mid-drag and open floats (chat, search-replace,
variables) only re-clamped on release. Read each boundary dimension from its
scoped element with a :root fallback, and observe the container elements as
well as :root, so an open float tracks the drag live exactly as before.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Added a second commit: the resize hooks now write the CSS var to the consuming container mid-drag, so use-float-boundary-sync (open chat / search-replace / variables floats) needed to read the scoped element values and observe those elements — otherwise an open float would only re-clamp on release instead of tracking live. Verified via CDP: the boundary observer fires once per drag frame and reads the live scoped width (417px) while :root is still the stale committed value (320px), reconciling on release. Resize perf with no float open is unchanged (8.3ms frames, 0 blocking).

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

…idth

- useDragResize: unmounting mid-drag now runs endDrag (commit + drop the
  scoped override) instead of a bare cleanup, so navigating away can neither
  lose the resize nor strand an inline override on a surviving target element.
- terminal output-panel clamp: read the live --output-panel-width from the
  terminal element first (where a drag writes its scoped override), then :root,
  then the store, so a mid-drag terminal/window resize can't clamp against a
  stale value.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/hooks/use-drag-resize.ts Outdated
…ount, clamp skip

Addresses three review findings on the scoped-resize change:
- useDragResize: track the last applied value and commit that on release;
  only recompute from the pointer event while the target is still connected.
  On an unmount the target's rect can be detached, so a layout-reading compute
  (e.g. the output panel's) would return a degenerate MIN — committing the last
  shown value avoids persisting the wrong width, and keeps flick-safety on a
  normal release.
- use-sidebar-resize: finalize on unmount (run endDrag, not bare cleanup) so a
  drag interrupted by unmount persists the width and drops the scoped override.
  This matters more than for the panel/terminal because .sidebar-shell-outer
  lives in the workspace chrome and outlives the sidebar, so a stranded override
  would win over :root.
- terminal output-panel clamp: skip while an output-panel drag is active (its
  scoped inline override is present). That drag's own compute clamps every frame
  against the live terminal rect, and a store-driven :root write here would be
  masked by the inline override anyway.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

- use-sidebar-resize: compute the clamped width synchronously on each pointer
  move (storing lastWidth) and defer only the DOM write to rAF, so a fast flick
  released before the frame runs still commits the final pointer position
  instead of a stale frame or nothing.
- terminal output-panel clamp: when not mid-drag, read the committed
  --output-panel-width from :root (written synchronously by the store setter)
  rather than the React store value, which lags a render behind the commit;
  fall back to the store before any commit. Comment corrected to match.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 4a8b2b9. Configure here.

@waleedlatif1
waleedlatif1 merged commit 92feb24 into staging Jul 17, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant